package com.stark.wechat.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import com.stark.wechat.form.Result;
import com.stark.wechat.message.response.Article;
import com.stark.wechat.message.response.Message;
import com.stark.wechat.message.template.TemplateMessage;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.core.util.QuickWriter;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import com.thoughtworks.xstream.io.xml.PrettyPrintWriter;
import com.thoughtworks.xstream.io.xml.XppDriver;

import net.sf.json.JSONObject;

/**
 * 微信开发工具类。
 * @author Ben
 * @since 1.0.0
 * @version 1.0.0, 2017/8/5
 * @see <a href="https://mp.weixin.qq.com/wiki">https://mp.weixin.qq.com/wiki</a>
 */
public class WechatUtil {
	/**
	 * 验证服务器地址的有效性。
	 * @param signature 微信加密签名， signature 结合了开发者填写的 token 参数和请求中的 timestamp 参数、 nonce 参数。
	 * @param timestamp 时间戳。
	 * @param nonce 随机数。
	 * @param echostr 随机字符串。
	 * @param token 微信公众号唯一凭证（在开发者中心中配置）。
	 * @return 验证通过时原样返回 echostr ，否则返回其他任意字符串。
	 */
	public static String validateAccess(String signature, String timestamp, String nonce, String echostr, String token) {
		// 1.将token、timestamp、nonce三个参数进行字典序排序
		String[] array = new String[] {token, timestamp, nonce};
		Arrays.sort(array);
		// 2. 将三个参数字符串拼接成一个字符串进行sha1加密
		String tmpStr = "";
		for (String str : array) {
			tmpStr += str;
		}
		tmpStr = DigestUtils.sha1Hex(tmpStr);
		// 3. 开发者获得加密后的字符串可与signature对比，标识该请求来源于微信
		boolean isValid = tmpStr.equalsIgnoreCase(signature);
		return isValid ? echostr : "error";
	}
	
	/**
	 * 生成 JS-SDK 签名。
	 * <p>签名用的 noncestr 和 timestamp 必须与 wx.config 中的 nonceStr 和 timestamp 相同。
	 * @param noncestr 随机字符串。
	 * @param jsapi_ticket 有效的 jsapi_ticket 。
	 * @param timestamp 时间戳。
	 * @param url 调用 JS 接口页面的完整 URL ，不包含 # 及其后面部分。
	 * @return JS-SDK 签名。
	 */
	public static String createJsSdkSignature(String noncestr, String jsapi_ticket, long timestamp, String url) {
		/*
		 * 步骤1. 对所有待签名参数按照字段名的ASCII 码从小到大排序（字典序）后，
		 * 使用URL键值对的格式（即key1=value1&key2=value2…）拼接成字符串string1
		 */
		String signature = new StringBuffer()
				.append("jsapi_ticket=").append(jsapi_ticket)
				.append("&noncestr=").append(noncestr)
				.append("&timestamp=").append(timestamp)
				.append("&url=").append(url)
				.toString();
		/*
		 * 步骤2. 对string1进行sha1签名，得到signature
		 */
		signature = DigestUtils.sha1Hex(signature);
		return signature;
	}
	
	/**
	 * 将回调 url 格式化成 Oauth2 认证的 url ，用于点击按钮跳转内部地址时附带用户信息。
	 * <p>如果用户同意授权，页面将跳转至 redirect_uri/?code=CODE&state=STATE ；
	 * 若用户禁止授权，则重定向后不会带上 code 参数，仅会带上 state 参数 redirect_uri?state=STATE 。
	 * code 作为换取 access_token 的票据，每次用户授权带上的 code 将不一样， code 只能使用一次， 5 分钟未被使用自动过期。
	 * @param appid 公众号的唯一标识。
	 * @param redirect_uri 授权后重定向的回调链接地址。
	 * @param scope <code>snsapi_base</code> ：不弹出授权页面，直接跳转，只能获取用户 openid ；
	 * <code>snsapi_userinfo</code> ：弹出授权页面，可通过 openid 拿到昵称、性别、所在地。
	 * 即使在未关注的情况下，只要用户授权，也能获取其信息。
	 * @param state 开发者可以自定义 a-zA-Z0-9 的参数值，最多 128 字节。
	 * @return Oauth2 认证的回调 url 。
	 * @throws UnsupportedEncodingException 字符串转码时抛出异常。
	 */
	public static String formatOauth2URL(String appid, String redirect_uri, String scope, String state)
			throws UnsupportedEncodingException {
		final String requestURL = "https://open.weixin.qq.com/connect/oauth2/authorize";
		redirect_uri = new StringBuffer()
				.append(requestURL)
				.append("?appid=").append(appid)
				.append("&redirect_uri=").append(URLEncoder.encode(redirect_uri, "UTF-8"))
				.append("&response_type=code")
				.append("&scope=").append(scope)
				.append("&state=").append(StringUtils.isBlank(state) ? "" : state)
				.append("#wechat_redirect")
				.toString();
		return redirect_uri;
	}
	
	/**
	 * 向微信请求 access_token 。
	 * <p>access_token 有效期 7200 秒，开发者必须在自己的服务全局缓存 access_token 。
	 * @param appID 第三方用户唯一凭证。
	 * @param appsecret 第三方用户唯一凭证密钥。
	 * @return {@link Result} 对象。请求成功时 {@link Result#getData()} 中包含属性： (String) access_token 、 (long) expires_in 。
	 */
	public static Result requestAccessToken(String appID, String appsecret) {
		final String requestURL = "https://api.weixin.qq.com/cgi-bin/token";
		Map<String, String> params = new HashMap<>();
		params.put("grant_type", "client_credential");
		params.put("appid", appID);
		params.put("secret", appsecret);
		String result = "";
		try {
			result = HttpClient.getInstance().sendGet(requestURL, params);
		} catch (Exception e) {
			return Result.fail().errcode(-1).errmsg("请求失败：" + e.getMessage());
		}
		
		JSONObject json = JSONObject.fromObject(result);
		// 成功
		if (!json.containsKey("errcode")) {
			String access_token = json.containsKey("access_token") ? json.getString("access_token") : "";
			long expires_in = json.containsKey("expires_in") ? json.getLong("expires_in") : 0;
			return Result.success().addData("access_token", access_token).addData("expires_in", expires_in);
		}
		// 失败
		int errcode = json.getInt("errcode");
		String errmsg = ErrorMap.getErrmsg(errcode);
		return Result.fail().errcode(errcode).errmsg(errmsg);
	}
	
	/**
	 * 向微信请求 jsapi_ticket （公众号用于调用微信 JS 接口的临时票据）。
	 * <p>jsapi_ticket 有效期 7200 秒，开发者必须在自己的服务全局缓存 jsapi_ticket 。
	 * @param access_token 全局身份票据。
	 * @return {@link Result} 对象。请求成功时 {@link Result#getData()} 中包含属性： (String) ticket 、 (long) expires_in 。
	 */
	public static Result requestJsapiTicket(String access_token) {
		final String requestURL = "https://api.weixin.qq.com/cgi-bin/ticket/getticket";
		Map<String, String> params = new HashMap<>();
		params.put("access_token", access_token);
		params.put("type", "jsapi");
		String result = "";
		try {
			result = HttpClient.getInstance().sendGet(requestURL, params);
		} catch (Exception e) {
			return Result.fail().errcode(-1).errmsg("请求失败：" + e.getMessage());
		}
		
		JSONObject json = JSONObject.fromObject(result);
		int errcode = -1;
		if (json.containsKey("errcode")) {
			errcode = json.getInt("errcode");
		}
		// 成功
		if (errcode == 0) {
			String ticket = json.getString("ticket");
			long expires_in = json.getLong("expires_in");
			return Result.success().addData("ticket", ticket).addData("expires_in", expires_in);
		}
		// 失败
		String errmsg = ErrorMap.getErrmsg(errcode);
		return Result.fail().errcode(errcode).errmsg(errmsg);
	}
	
	/**
	 * 在网页授权回调应用内部地址时，向微信请求 openId 。
	 * <p>由于公众号的 secret 和获取到的 access_token 安全级别都非常高，必须只保存在服务器，不允许传给客户端。
	 * 后续刷新 access_token 、通过 access_token 获取用户信息等步骤，也必须从服务器发起。
	 * @param appid 公众号的唯一标识。
	 * @param secret 公众号的 appsecret 。
	 * @param code 填写 {@link WechatUtil#formatOauth2URL} 获取的 code 参数。
	 * @return {@link Result} 对象。请求成功时 {@link Result#getData()} 中包含属性： (String) access_token 、 (long) expires_in 、 
	 * (String) refresh_token 、 (String) openid 、 (String) scope 。
	 */
	public static Result requestOauth2UserInfo(String appid, String secret, String code) {
		final String requestURL = "https://api.weixin.qq.com/sns/oauth2/access_token";
		Map<String, String> params = new HashMap<>();
		params.put("appid", appid);
		params.put("secret", secret);
		params.put("code", code);
		params.put("grant_type", "authorization_code");
		String result = "";
		try {
			result = HttpClient.getInstance().sendGet(requestURL, params);
		} catch (Exception e) {
			return Result.fail().errcode(-1).errmsg("请求失败：" + e.getMessage());
		}
		
		JSONObject json = JSONObject.fromObject(result);
		if (!json.containsKey("errcode")) {
			String access_token = json.getString("access_token");
			long expires_in = json.getLong("expires_in");
			String refresh_token = json.getString("refresh_token");
			String openid = json.getString("openid");
			String scope = json.getString("scope");
			return Result.success().addData("access_token", access_token)
					.addData("expires_in", expires_in)
					.addData("refresh_token", refresh_token)
					.addData("openid", openid)
					.addData("scope", scope);
		}
		int errcode = json.getInt("errcode");
		String errmsg = ErrorMap.getErrmsg(errcode);
		return Result.fail().errcode(errcode).errmsg(errmsg);
	}
	
	/**
	 * 更新自定义菜单。
	 * @param access_token 全局身份票据。
	 * @param menuJsonStr json 字符串格式的菜单内容。
	 * @return {@link Result} 对象。
	 */
	public static Result updateCustomMenu(String access_token, String menuJsonStr) {
		final String requestURL = "https://api.weixin.qq.com/cgi-bin/menu/create";
		String url = requestURL + "?access_token=" + access_token;
		String result = "";
		try {
			result = HttpClient.getInstance().sendPost(url, menuJsonStr);
		} catch (Exception e) {
			return Result.fail().errcode(-1).errmsg("请求失败：" + e.getMessage());
		}
		
		JSONObject json = JSONObject.fromObject(result);
		int errcode = -1;
		if (json.containsKey("errcode")) {
			errcode = json.getInt("errcode");
		}
		if (errcode == 0) {
			return Result.success();
		}
		String errmsg = ErrorMap.getErrmsg(errcode);
		return Result.fail().errcode(errcode).errmsg(errmsg);
	}
	
    /**
	 * 推送模板消息。
	 * @param access_token 全局身份票据。
	 * @param messageJsonStr json 字符串格式的模板消息。
	 * @return {@link Result} 对象。请求成功时 {@link Result#getData()} 中包含属性： (long) msgid 。
	 */
	public static Result pushTemplateMessage(String access_token, String messageJsonStr) {
		final String requestURL = "https://api.weixin.qq.com/cgi-bin/message/template/send";
		String url = requestURL + "?access_token=" + access_token;
		String result = "";
		try {
			result = HttpClient.getInstance().sendPost(url, messageJsonStr);
		} catch (Exception e) {
			return Result.fail().errcode(-1).errmsg("请求失败：" + e.getMessage());
		}
		
		JSONObject json = JSONObject.fromObject(result);
		int errcode = -1;
		if (json.containsKey("errcode")) {
			errcode = json.getInt("errcode");
		}
		if (errcode == 0) {
			long msgid = json.getLong("msgid"); 
			return Result.success().addData("msgid", msgid);
		}
		String errmsg = json.containsKey("errmsg") ? json.getString("errmsg") : ErrorMap.getErrmsg(errcode);
		return Result.fail().errcode(errcode).errmsg(errmsg);
	}
	
	/**
	 * 推送模板消息。
	 * @param access_token 全局身份票据。
	 * @param templateMessage 模板消息对象。
	 * @return {@link Result} 对象。请求成功时 {@link Result#getData()} 中包含属性： (long) msgid 。
	 * @see #pushTemplateMessage(String, String)
	 */
	public static Result pushTemplateMessage(String access_token, TemplateMessage templateMessage) {
		String messageJsonStr = JSONObject.fromObject(templateMessage).toString();
		Result result = WechatUtil.pushTemplateMessage(access_token, messageJsonStr);
		return result;
	}
	
	/**
     * 解析微信发来的请求（ XML ）。
     * @param request HTTP 请求对象。
     * @return 解析 XML 获得的参数 {@link HashMap} 。
	 * @throws IOException 获取输入流时抛出异常。
	 * @throws DocumentException 读取 XML 时抛出异常。
     */
    public static Map<String, String> parseXmlRequest(HttpServletRequest request) throws IOException, DocumentException {  
        InputStream is = null;
        Document document = null;
        try {
	        	is = request.getInputStream();
	        	SAXReader reader = new SAXReader();
	        	document = reader.read(is);
        } finally {
			if (is != null) {
				try {
					is.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
        Element root = document.getRootElement();
        List<Element> elementList = root.elements();
        
        // 遍历所有子节点
        Map<String, String> map = new HashMap<>();
        for (Element element : elementList) {
            map.put(element.getName(), element.getText());
        }
        return map;
    }
    
    /**
     * 把消息对象转成 XML 。
     * @param message 消息对象。
     * @return 消息对象转成的 XML 字符串。
     */
	public static String messageToXml(Message message) {
		/**
		 * 扩展xstream，使其支持CDATA块
		 */
		XStream xstream = new XStream(new XppDriver() {
			public HierarchicalStreamWriter createWriter(Writer out) {
				return new PrettyPrintWriter(out) {
					public void startNode(String name, @SuppressWarnings("rawtypes") Class clazz) {
						super.startNode(name, clazz);
					}

					protected void writeText(QuickWriter writer, String text) {
						try {
							Double.parseDouble(text);
							// text 是数值类型
							writer.write(text);
						} catch (NumberFormatException e) {
							// text 不是数值类型
							writer.write("<![CDATA[");
							writer.write(text);
							writer.write("]]>");
						}
					}
				};
			}
		});

		xstream.alias("xml", message.getClass());
		xstream.alias("item", Article.class);
		String xmlStr = xstream.toXML(message);
		return xmlStr;
	}
}
